Phase 3: Timed Motor Control (v1.2.0)#5
Merged
Conversation
…TRL-01) - Add test_timed_open_sends_command_immediately: asserts CMD_UP awaited once on async_open_cover for CONF_BIDIRECTIONAL=False cover, no event needed - Add test_timed_close_sends_command_immediately: asserts CMD_DOWN awaited once on async_close_cover for timed motor - Add CMD_UP, CMD_DOWN, CMD_STOP to const import block for use in tests
…-07) - __init__: compute _is_calibrated from VALUE-presence of CONF_OPEN_TIME and CONF_CLOSE_TIME (is not None) — key-present-but-None stays uncalibrated (REVIEW-01) - _unrecorded_attributes: add 'calibrated' alongside 'mode' (D-07, T-03-04) - extra_state_attributes: expose calibrated key for timed motors only (D-07) - _handle_calibration_completed: set _is_calibrated=True before async_write_ha_state (REVIEW-05) - tests: 4 failing-first tests (RED confirmed) then GREEN for D-06/D-07 behaviours
- async_set_cover_position: early-return guard at method top under `if not self._is_bidirectional and not self._is_calibrated:` with a single debug log; SET_POSITION feature retained (slider stays visible) - test: failing-first (RED confirmed) then GREEN for D-05 behaviour
…DD GREEN) - Extract _restore_position_from_last_state helper (REVIEW-02): the generic recorded-position restore logic now lives in one place, called from both the bidirectional and timed-idle branches — no duplication, no drift risk. - Restructure async_added_to_hass: timed branch (not _is_bidirectional) snaps opening→100%% and closing→0%% on restart (D-08); bidirectional branch calls the shared helper unchanged (D-10). - Add if not self._is_bidirectional: return guard at the top of _handle_event (D-11/REVIEW-04) — stray inbound events on timed motors are a structural no-op, never mutating state or writing HA state. - Tests: test_timed_restart_opening_snaps_to_100, test_timed_restart_closing_snaps_to_0, test_timed_handle_event_ignored (all GREEN); bidirectional restore canary passes.
…tors (TDD GREEN) - Timed-idle restart branch calls _restore_position_from_last_state (shared helper) — a real recorded 0%% is preserved via 'is None' sentinel, never replaced by initial_position (Pitfall 1 / REVIEW-02 — no inline duplication). - No-prior-state fallback for timed motors is now 100%% (assume open, D-09), NOT the existing bidirectional 0%% default; the existing else:0 is gated on self._is_bidirectional so bidirectional behavior is unchanged (D-10). - Tests: test_timed_restart_idle_restores_position (real-0%% key guard), test_timed_restart_no_prior_state_uses_initial_position, test_timed_restart_no_prior_no_initial_defaults_to_100 (all GREEN).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 3: Timed Motor Control · v1.2.0
Goal: Non-bidirectional (timed) motors respond immediately to open/close/stop commands, estimate position from elapsed time, and survive restarts with position intact.
Status: Verified ✓ (13/13 must-haves) · threat-secure · Nyquist-compliant
Adds first-class support for timed (non-bidirectional) roller-shutter motors that never report status back. Open/close/stop now dispatch immediately (no waiting for a motor confirmation that never arrives); a mid-travel Stop freezes the time-based position estimate and a full run resets to the endstop; an uncalibrated motor's set-position is an inert-but-visible no-op until Phase 4 calibration; and a timed motor's position survives a Home Assistant restart. Existing bidirectional motors are byte-for-byte unaffected.
Changes
03-01 — Immediate control (CTRL-01/02)
Characterization tests pinning immediate CMD_UP/CMD_DOWN dispatch, stop-freezes-estimate, and full-run reset to 100%/0% for timed motors. Tests only — no
cover.pychange.03-02 — Calibrated gate (CTRL-02)
_is_calibratedflag (computed from value-presence of persisted open/close times, not key-presence), a timed-onlycalibratedstate attribute (suppressed from HA history via_unrecorded_attributes), and a no-op gate so set-position on an uncalibrated timed motor is silently ignored while the slider stays visible.03-03 — Restart survival (CTRL-04)
async_added_to_hassrestores timed motors after a restart: mid-move snaps to the destination endstop (opening→100, closing→0); idle restore preserves the recorded position including a genuine 0%, falling back to initial_position then 100% — never collapsing missing data to 0%. Restore logic is extracted into a shared_restore_position_from_last_statehelper used by both the bidirectional and timed paths, and_handle_eventearly-returns for timed motors.03-04 — Zero regression (CTRL-05)
Behavior-assertion tests proving the bidirectional path (open/close/stop, ungated set-position, restore, no
calibratedattribute, legacy no-flag default) is unaffected by every Phase 3 change.Key files:
custom_components/schellenberg_usb/cover.py,tests/test_cover.py,custom_components/schellenberg_usb/manifest.json(1.1.2 → 1.2.0).Requirements Addressed
Verification
03-SECURITY.md— 9 threats,threats_open: 0(all low, accepted/mitigated)_target_position) + 4 warnings fixed, with a regression testKey Decisions
calibratedattribute (unrecorded), inert-but-visible set-position until calibrated.🤖 Generated with Claude Code
https://claude.ai/code/session_01QwPMgtiypLgJ5nkR3mUGfL